<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package	Zend_Auth
 * @subpackage Zend_Auth_Adapter
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license	http://framework.zend.com/license/new-bsd	 New BSD License
 * @version	$Id: OpenId.php 16200 2009-06-21 18:50:06Z thomas $
 */

/**
 * @see Zend_Auth_Adapter_Interface
 */
require_once 'Zend/Auth/Adapter/Interface.php';

/**
 * @see Zend_OpenId_Consumer
 */
require_once 'Zend/OpenId/Consumer.php';
require_once 'Core/Auth/Adapter/OpenId.php';

/**
 * A Zend_Auth Authentication Adapter allowing the use of OpenID protocol as an
 * authentication mechanism
 *
 * @category   Zend
 * @package	Zend_Auth
 * @subpackage Zend_Auth_Adapter
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license	http://framework.zend.com/license/new-bsd	 New BSD License
 */
class Core_Auth_Adapter_Google implements Zend_Auth_Adapter_Interface {
	
	/**
	 * Google OpenID Discovery URL
	 * 
	 * @var string
	 */
	const GOOGLE_DISCOVER_URL = "https://www.google.com/accounts/o8/id";
	
	/**
	 * Namespace for OpenID v2.0
	 */
	const OPENID_NAMESPACE = "http://specs.openid.net/auth/2.0";

	/**
	 * OpenID Mode: Check and setup.
	 * 
	 * @var string
	 */
	const MODE_CHECKID = 'checkid_setup';
	
	/**
	 * OpenID Mode: Resolve identity
	 * 
	 * @var string
	 */
	const MODE_ID_RES = 'id_res';
	
	/**
	 * OpenID Mode: Cancel
	 * 
	 * @var string
	 */
	const MODE_CANCEL = 'cancel';
	
	/**
	 * OpenID Mode: Check Authentication
	 * 
	 * @var string
	 */
	const MODE_CHECK_AUTHENTICATION = 'check_authentication';
	
	/**
	 * the mode (checkid_setup, id_res, or cancel)
	 * 
	 * @var string
	 */
	private $mode = self::MODE_CHECKID;
	
	/**
	 * The operational endpoint for the OAuth Request. Autopopulated
	 * during authentication.
	 * 
	 * @var string
	 */
	private $endpoint = '';
	
	/**
	 * Nonce retrieved by the response.
	 * 
	 * @var string
	 */
	private $response_nonce = null;
	
	/**
	 * Return url
	 * 
	 * @var string
	 */
	private $return_to = null;
	
	/**
	 * The realm that the user is being asked to trust. Derived from the
	 * return url.
	 * 
	 * @var string
	 */
	private $realm = null; //the realm that the user is being asked to trust
	
	/**
	 * Association handle between this service and Google
	 * 
	 * @var string
	 */
	private $assoc_handle = null;
	
	/**
	 * ID Claimed by the user.
	 * 
	 * @var string
	 */
	private $claimed_id = "http://specs.openid.net/auth/2.0/identifier_select";
	
	
	/**
	 * For google, this is the same as claimed_id
	 * 
	 * @var string
	 */
	private $identity = "http://specs.openid.net/auth/2.0/identifier_select";

	/**
	 * Is this a signed request?
	 * 
	 * @var boolean
	 */
	private $signed = null;
	
	/**
	 * Signature
	 * 
	 * @var string
	 */
	private $sig = null;
	
	/**
	 * The user's email address
	 * 
	 * @var string
	 */
	private $email = null; //the user's email address
	
	/**
	 * Do we want this user's email address?
	 * 
	 * @var boolean
	 */
	private $require_email = true; //the user's email address
	
	/**
	 * Constructor for a new Google Authentication handler. 
	 * 
	 * @param string $return_to
	 */
	public function __construct($return_to) {
		
		// Store return_to.
		$this->return_to = $return_to;
		
		// Extract the domain realm from the return_to.
		$return_pieces = parse_url ( $this->return_to );
		$this->realm = $return_pieces ['scheme'] . "://" . $return_pieces ['host'];
	}
	
	/**
	 * @see Core_Auth_Adapter_OpenId::authenticate()
	 */
	public function authenticate() {
		
		$this->endpoint = $this->getEndPoint();
		$this->assoc_handle = $this->getAssociationHandle ();
		
		$request = Zend_Controller_Front::getInstance()->getRequest();
		$params = $this->validateResponse($request->getParams());
		
		$this->mode = isset($params['openid_mode']) ? $params['openid_mode'] : self::MODE_CHECKID;
		
		// Do we have to redirect?
		if ( $this->mode == self::MODE_CHECKID)
		{
			// We're going to redirect.
			$url = $this->getRedirectURL();
			return new Zend_Auth_Result ( Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID, null, array ('redirect' => $url ) );
		}
		else if ( $this->mode == self::MODE_ID_RES )
		{
			// We've got valid, signed return data.
			$email = $params['openid_ext1_value_email'];
			return new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $email, $params);
		}
		
		return new Zend_Auth_Result ( Zend_Auth_Result::FAILURE, null );
	}
	
	/**
	 * This method fetches an association handle from google. These are valid for two 
	 * weeks, so you can retrieve it yourself and cache it for future usage.
	 */
	private function getAssociationHandle($endpoint = null) {
		
		// Construct our Association request url.
		$params = array ('openid.ns' => self::OPENID_NAMESPACE, 'openid.mode' => 'associate', 'openid.assoc_type' => 'HMAC-SHA1', 'openid.session_type' => 'no-encryption' );
		$url = sprintf ( '%s?%s', $this->endpoint, http_build_query ( $params ) );
		
		// Get the association handle date.
		$client = new Zend_Http_Client ( $url );
		$result = $client->request ( Zend_Http_Client::GET );
		if ($result->isError ()) {
			throw new Zend_OpenId_Exception ( Zend_OpenId_Exception::ERROR_STORAGE, 'Invalid result from association handle request.' );
		}
		
		// Parse the data.
		$body = explode ( "\n", $result->getBody () );
		
		// Loop through each line
		foreach ( $body as $line ) {
			$var = explode ( ':', $line );
			if ($var [0] == 'assoc_handle') {
				return $var [1];
			}
		}
		
		throw new Zend_OpenId_Exception ( Zend_OpenId_Exception::ERROR_STORAGE, 'No association handle found.' );
	}
	
	/**
	 * This method is a poor-man's implementation of Yadis discovery. It parses
	 * Google's yadis xml structure for the OpenID endpoint that we're looking for.
	 * 
	 * @throws Zend_OpenId_Exception
	 */
	private function getEndPoint() {
		$client = new Zend_Http_Client ( self::GOOGLE_DISCOVER_URL );
		$result = $client->request ( Zend_Http_Client::GET );
		
		try {
			if ($result->isError ()) {
				throw new Zend_OpenId_Exception ( Zend_OpenId_Exception::ERROR_STORAGE, 'Invalid result from endpoint request.' );
			}
			
			$domdoc = new DOMDocument ();
			$domdoc->loadXML ( $result->getBody () );
			
			$uri = $domdoc->getElementsByTagName ( "URI" );
			$uri = $uri->item ( 0 )->nodeValue;
			
			return $uri;
		} catch ( Exception $e ) {
			throw new Zend_OpenId_Exception ( Zend_OpenId_Exception::ERROR_STORAGE, 'Invalid result from endpoint request.' );
		}
	}

	/**
	 * Construct the GET request redirect url that we need to send the user to so 
	 * they can authenticate us.
	 */
	private function getRedirectURL()
	{
		// OpenID request parameters
		$params = array(
			'openid.ns' => self::OPENID_NAMESPACE,
			'openid.claimed_id' => $this->claimed_id,
			'openid.identity' => $this->identity,
			'openid.return_to' => $this->return_to,
			'openid.realm' => $this->realm,
			'openid.assoc_handle' => $this->assoc_handle,
			'openid.mode' => $this->mode,
			'openid.ns.ext1' => "http://openid.net/srv/ax/1.0",
			'openid.ext1.mode' => "fetch_request",
			'openid.ext1.type.email' => "http://schema.openid.net/contact/email",
			'openid.ext1.type.firstName' => "http://axschema.org/namePerson/first",
			'openid.ext1.type.lastName' => "http://axschema.org/namePerson/last",
			'openid.ext1.required' =>  "email,firstName,lastName"
		);
		
		return sprintf ( '%s?%s', $this->endpoint, http_build_query ( $params ) );
	}
	
	/**
	 * Validate the request response.
	 * 
	 * @param array $params
	 */
	private function validateResponse( $params )
	{
		$signParams = array();
		
		// Only bother to check resolution signatures.
		if ( empty($params['openid_mode']) || $params['openid_mode'] != self::MODE_ID_RES)
		{
			return $signParams;
		}
		
		// First, extract all the parameter from the signed parameters list.
		$signParams['openid.signed'] = $params['openid_signed'];
		$signParams['openid.sig'] = $params['openid_sig'];
		$signParams['openid.mode'] = self::MODE_CHECK_AUTHENTICATION;
		
		$parameterKeys = explode(',', $params['openid_signed']);
		foreach ($parameterKeys as $key)
		{
			$paramKey = 'openid_' . str_replace('.', '_', $key);
			$signParams["openid.$key"] = $params[$paramKey];
		}
		
		$url = sprintf('%s?%s', $params['openid_op_endpoint'], http_build_query($signParams));
		$client = new Zend_Http_Client($url);
		$result = $client->request( Zend_Http_Client::GET);
		
		if ( !$result->isError())
		{
			$body = $result->getBody();
			if(strpos($body, 'is_valid:true') !== false)
			{
				return $params;
			}
		}
		return array();
	}
}